// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl.tasks;

import java.net.InetAddress;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TimerTask;

import javax.jmdns.impl.DNSConstants;
import javax.jmdns.impl.DNSEntry;
import javax.jmdns.impl.DNSIncoming;
import javax.jmdns.impl.DNSOutgoing;
import javax.jmdns.impl.DNSQuestion;
import javax.jmdns.impl.DNSRecord;
import javax.jmdns.impl.DNSState;
import javax.jmdns.impl.JmDNSImpl;
import javax.jmdns.impl.ServiceInfoImpl;

import android.util.Log;

/**
 * The Responder sends a single answer for the specified service infos and for
 * the host name.
 */
public class Responder extends TimerTask {
   public final static String TAG = Responder.class.toString();

   /**
     * 
     */
   private final JmDNSImpl jmDNSImpl;
   private DNSIncoming in;
   private InetAddress addr;
   private int port;

   public Responder(JmDNSImpl jmDNSImpl, DNSIncoming in, InetAddress addr, int port) {
      this.jmDNSImpl = jmDNSImpl;
      this.in = in;
      this.addr = addr;
      this.port = port;
   }

   public void start() {
      // According to draft-cheshire-dnsext-multicastdns.txt
      // chapter "8 Responding":
      // We respond immediately if we know for sure, that we are
      // the only one who can respond to the query.
      // In all other cases, we respond within 20-120 ms.
      //
      // According to draft-cheshire-dnsext-multicastdns.txt
      // chapter "7.2 Multi-Packet Known Answer Suppression":
      // We respond after 20-120 ms if the query is truncated.

      boolean iAmTheOnlyOne = true;
      for (Iterator i = in.getQuestions().iterator(); i.hasNext();) {
         DNSEntry entry = (DNSEntry) i.next();
         if (entry instanceof DNSQuestion) {
            DNSQuestion q = (DNSQuestion) entry;
            iAmTheOnlyOne &= (q.getType() == DNSConstants.TYPE_SRV || q.getType() == DNSConstants.TYPE_TXT
                     || q.getType() == DNSConstants.TYPE_A || q.getType() == DNSConstants.TYPE_AAAA
                     || this.jmDNSImpl.getLocalHost().getName().equalsIgnoreCase(q.getName()) || this.jmDNSImpl
                     .getServices().containsKey(q.getName().toLowerCase()));
            if (!iAmTheOnlyOne) {
               break;
            }
         }
      }
      int delay = (iAmTheOnlyOne && !in.isTruncated()) ? 0 : DNSConstants.RESPONSE_MIN_WAIT_INTERVAL
               + JmDNSImpl.getRandom().nextInt(
                        DNSConstants.RESPONSE_MAX_WAIT_INTERVAL - DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + 1)
               - in.elapseSinceArrival();
      if (delay < 0) {
         delay = 0;
      }
      this.jmDNSImpl.schedule(this, delay);
   }

   public void run() {
      synchronized (this.jmDNSImpl.getIoLock()) {
         if (this.jmDNSImpl.getPlannedAnswer() == in) {
            this.jmDNSImpl.setPlannedAnswer(null);
         }

         // We use these sets to prevent duplicate records
         // FIXME - This should be moved into DNSOutgoing
         HashSet questions = new HashSet();
         HashSet answers = new HashSet();

         if (this.jmDNSImpl.getState() == DNSState.ANNOUNCED) {
            try {
               boolean isUnicast = (port != DNSConstants.MDNS_PORT);

               // Answer questions
               for (Iterator iterator = in.getQuestions().iterator(); iterator.hasNext();) {
                  DNSEntry entry = (DNSEntry) iterator.next();
                  if (entry instanceof DNSQuestion) {
                     DNSQuestion q = (DNSQuestion) entry;

                     // for unicast responses the question must be included
                     if (isUnicast) {
                        // out.addQuestion(q);
                        questions.add(q);
                     }

                     int type = q.getType();
                     if (type == DNSConstants.TYPE_ANY || type == DNSConstants.TYPE_SRV) { // I
                                                                                           // ama
                                                                                           // not
                                                                                           // sure
                                                                                           // of
                                                                                           // why
                                                                                           // there
                                                                                           // is
                                                                                           // a
                                                                                           // special
                                                                                           // case
                                                                                           // here
                                                                                           // [PJYF
                                                                                           // Oct
                                                                                           // 15
                                                                                           // 2004]
                        if (this.jmDNSImpl.getLocalHost().getName().equalsIgnoreCase(q.getName())) {
                           // type = DNSConstants.TYPE_A;
                           DNSRecord answer = this.jmDNSImpl.getLocalHost().getDNS4AddressRecord();
                           if (answer != null) {
                              answers.add(answer);
                           }
                           answer = this.jmDNSImpl.getLocalHost().getDNS6AddressRecord();
                           if (answer != null) {
                              answers.add(answer);
                           }
                           type = DNSConstants.TYPE_IGNORE;
                        } else {
                           if (this.jmDNSImpl.getServiceTypes().containsKey(q.getName().toLowerCase())) {
                              type = DNSConstants.TYPE_PTR;
                           }
                        }
                     }

                     switch (type) {
                     case DNSConstants.TYPE_A: {
                        // Answer a query for a domain name
                        // out = addAnswer( in, addr, port, out, host );
                        DNSRecord answer = this.jmDNSImpl.getLocalHost().getDNS4AddressRecord();
                        // Log.d(TAG,
                        // String.format("trying to answer TYPE_A with answer=%s",
                        // answer.toString()));
                        if (answer != null) {
                           answers.add(answer);
                        }
                        break;
                     }
                     case DNSConstants.TYPE_AAAA: {
                        // Answer a query for a domain name
                        DNSRecord answer = this.jmDNSImpl.getLocalHost().getDNS6AddressRecord();
                        if (answer != null) {
                           answers.add(answer);
                        }
                        break;
                     }
                     case DNSConstants.TYPE_PTR: {
                        // Answer a query for services of a given type

                        // find matching services
                        for (Iterator serviceIterator = this.jmDNSImpl.getServices().values().iterator(); serviceIterator
                                 .hasNext();) {
                           ServiceInfoImpl info = (ServiceInfoImpl) serviceIterator.next();
                           if (info.getState() == DNSState.ANNOUNCED) {
                              if (q.getName().equalsIgnoreCase(info.getType())) {
                                 DNSRecord answer = this.jmDNSImpl.getLocalHost().getDNS4AddressRecord();
                                 if (answer != null) {
                                    answers.add(answer);
                                 }
                                 answer = this.jmDNSImpl.getLocalHost().getDNS6AddressRecord();
                                 if (answer != null) {
                                    answers.add(answer);
                                 }
                                 answers.add(new DNSRecord.Pointer(info.getType(), DNSConstants.TYPE_PTR,
                                          DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()));
                                 answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
                                          DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info
                                                   .getPriority(), info.getWeight(), info.getPort(), this.jmDNSImpl
                                                   .getLocalHost().getName()));
                                 answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT,
                                          DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info
                                                   .getText()));
                              }
                           }
                        }
                        if (q.getName().equalsIgnoreCase("_services._mdns._udp.local.")) {
                           for (Iterator serviceTypeIterator = this.jmDNSImpl.getServiceTypes().values().iterator(); serviceTypeIterator
                                    .hasNext();) {
                              answers
                                       .add(new DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR,
                                                DNSConstants.CLASS_IN, DNSConstants.DNS_TTL,
                                                (String) serviceTypeIterator.next()));
                           }
                        }
                        break;
                     }
                     case DNSConstants.TYPE_SRV:
                     case DNSConstants.TYPE_ANY:
                     case DNSConstants.TYPE_TXT: {
                        ServiceInfoImpl info = (ServiceInfoImpl) this.jmDNSImpl.getServices().get(
                                 q.getName().toLowerCase());
                        if (info != null && info.getState() == DNSState.ANNOUNCED) {
                           DNSRecord answer = this.jmDNSImpl.getLocalHost().getDNS4AddressRecord();
                           if (answer != null) {
                              answers.add(answer);
                           }
                           answer = this.jmDNSImpl.getLocalHost().getDNS6AddressRecord();
                           if (answer != null) {
                              answers.add(answer);
                           }
                           answers.add(new DNSRecord.Pointer(info.getType(), DNSConstants.TYPE_PTR,
                                    DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()));
                           answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
                                    DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info
                                             .getPriority(), info.getWeight(), info.getPort(), this.jmDNSImpl
                                             .getLocalHost().getName()));
                           answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT,
                                    DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info
                                             .getText()));
                        }
                        break;
                     }
                     default: {
                        // System.out.println("JmDNSResponder.unhandled query:"+q);
                        break;
                     }
                     }
                  }
               }

               // remove known answers, if the ttl is at least half of
               // the correct value. (See Draft Cheshire chapter 7.1.).
               for (Iterator i = in.getAnswers().iterator(); i.hasNext();) {
                  DNSRecord knownAnswer = (DNSRecord) i.next();
                  if (knownAnswer.getTtl() > DNSConstants.DNS_TTL / 2 && answers.remove(knownAnswer)) {
                     Log.d(TAG, "JmDNS Responder Known Answer Removed");
                  }
               }

               // responde if we have answers
               if (answers.size() != 0) {
                  Log.d(TAG, "run() JmDNS responding");
                  DNSOutgoing out = null;
                  if (isUnicast) {
                     out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false);
                  }

                  for (Iterator i = questions.iterator(); i.hasNext();) {
                     out.addQuestion((DNSQuestion) i.next());
                  }
                  for (Iterator i = answers.iterator(); i.hasNext();) {
                     out = this.jmDNSImpl.addAnswer(in, addr, port, out, (DNSRecord) i.next());
                  }
                  this.jmDNSImpl.send(out);
               }
               this.cancel();
            } catch (Throwable e) {
               Log.d(TAG, "run() exception: " +  e.getMessage());
               this.jmDNSImpl.close();
            }
         }
      }
   }
}